package msu.ml.data;

import msu.ml.core.*;
import weka.core.*;
import org.w3c.dom.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import javax.xml.parsers.*;
import java.io.*;
import java.util.*;
import java.text.*;

/**
 * The validation post processor keeps track of validation information
 * (#correct vs #incorrect) and outputs accuracy and classification
 * information at the end of every sweep and experiment.
 *
 * @author Reginald M Mead
 * @version 1.0
 */
public class ValidationPostProcessor extends DataPostProcessorAdapter
{
    protected boolean verbose;
    protected Document experiment;
    protected Document results;
    protected Element currentElement;

    protected String output;
    protected String transformation;

    protected ClassificationRule pulseClassificationRule;
    protected ClassificationRule sweepClassificationRule;
    protected String name;
    protected int trueValue;
    protected int numClasses;
    protected int [] freqs;
    protected int correct;
    protected int total;
    protected long startTime;
    protected long endTime;

    public ValidationPostProcessor() throws javax.xml.parsers.ParserConfigurationException
    {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        results = builder.newDocument();

        this.verbose = ApplicationSettings.getInstance().getConfig(
                "verbose").equals("true");
    }

    /**
     * Initializes running counts etc.
     */
    public void beginExperiment(Experiment e)
    {
        super.beginExperiment(e);

        total = correct = 0;


        //XML
        currentElement = results.createElement("experiment_output");
        currentElement.setAttribute("version", "2.0");
        currentElement.appendChild(results.importNode(
                    experiment.getDocumentElement(), true));
        results.appendChild(currentElement);
        Element eResults = results.createElement("results");
        currentElement.appendChild(eResults);
        currentElement = eResults;


        //console output
        if(verbose)
            System.out.println("-----------------------");

        this.startTime = System.currentTimeMillis();
    }

    /**
     * Signals that training has begun;
     * 
     * @param labels training data labels
     */
    public void beginTraining(String [] labels)
    {
        //currentElement = <results> or <target_files>
        if(currentElement.getNodeName().equals("target_files"))
            currentElement = (Element)currentElement.getParentNode().getParentNode();
        Element eResult = results.createElement("result");
        Element eTrainingFiles = results.createElement("training_files");

        for(int i = 0; i < labels.length; i++)
        {
            Element eTrainingFile = results.createElement("training_file");
            eTrainingFile.setTextContent(labels[i]);
            eTrainingFiles.appendChild(eTrainingFile);
        }

        Element eTargetFiles = results.createElement("target_files");

        eResult.appendChild(eTrainingFiles);
        eResult.appendChild(eTargetFiles);
        currentElement.appendChild(eResult);
        currentElement = eTargetFiles;
    }

    /**
     * initializes sweep validation information 
     * @param name Name of sweep (usually file name)
     */
    public void beginInstances(String name)
    {
        this.name = name;
        trueValue = -1;
        this.freqs = new int[numClasses];

        //currentElement = <target_files>
        Element eTargetFile = results.createElement("target_file");
        eTargetFile.setAttribute("name", name);
        currentElement.appendChild(eTargetFile);
        currentElement = eTargetFile;
    }

    /**
     * Outputs Experiment data such as # of sweeps and accuracy
     */
    public void endExperiment()
    {
        this.endTime = System.currentTimeMillis();

        Element eDate = results.createElement("date");
        Date date = new Date();
        date.setTime(this.startTime);
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        eDate.setTextContent(dateFormat.format(date));

        DateFormat timeFormat = new SimpleDateFormat("HH:mm:ss: SSS");
        Element eStartTime = results.createElement("start_time");
        Date startTime = new Date();
        startTime.setTime(this.startTime);
        eStartTime.setTextContent(timeFormat.format(startTime));

        Element eEndTime = results.createElement("end_time");
        Date endTime = new Date();
        endTime.setTime(this.endTime);
        eEndTime.setTextContent(timeFormat.format(endTime));

        Element temp = this.results.getDocumentElement();
        temp.insertBefore(eEndTime, temp.getFirstChild());
        temp.insertBefore(eStartTime, temp.getFirstChild());
        temp.insertBefore(eDate, temp.getFirstChild());

        Element eSummaryStatistics = results.createElement("summary_statistics");
        Element eCount = results.createElement("count");
        eCount.setTextContent("" + total);
        Element eAccuracy = results.createElement("accuracy");
        eAccuracy.setTextContent("" + ((double)correct / (double)total * 100.0));
        eSummaryStatistics.appendChild(eCount);
        eSummaryStatistics.appendChild(eAccuracy);

        this.results.getDocumentElement().appendChild(eSummaryStatistics);

        transformDocument(this.results);

        // Console Ouput
        if(verbose)
        {
            System.out.println("-----------------------");
            System.out.println("-----------------------");
            System.out.println("" + total + " Sweeps");  
            System.out.println("" + correct + " Correct, " + (total - correct) 
                    + " Incorrect, " + ((double)correct / (double)total * 100.0) + "% Accurate");
            System.out.println();
        }
    }

    /**
     * Classifies sweep and outputs relevant information.
     */
    public void endInstances()
    {
        int sweepClass = this.sweepClassificationRule.getClassFromDistribution(freqs);
        total++;
        if(sweepClass == trueValue)
            correct++;

        //currentElement = <target_file>
        currentElement.setAttribute("true_class", "" + trueValue);
        currentElement.setAttribute("predicted_class", "" + sweepClass);
        Element eDistribution = results.createElement("distribution");
        for(int i = 0; i < freqs.length; i++)
        {
            Element eClass = results.createElement("class");
            eClass.setAttribute("index", "" + i);
            eClass.setTextContent("" + freqs[i]);
            eDistribution.appendChild(eClass);
        }
        currentElement.appendChild(eDistribution);
        currentElement = (Element)currentElement.getParentNode();

        // Console Ouput
        if(verbose)
        {
            System.out.println(name);
            System.out.println("Classification: " + sweepClass);
            System.out.println("True Class: " + trueValue);
            System.out.print("Frequencies : [");
            for(int i = 0; i < freqs.length; i++)
                System.out.print("" + freqs[i] + ", ");
            System.out.println("]");
            System.out.println();
        }
    }

    public Document getExperimentDocument()
    {
        return this.experiment;
    }

    /** Returns number of potential classes.
     * @return number of potential classes
     */
    public int getNumClasses()
    {
        return this.numClasses;
    }

    public String getOutput()
    {
        return this.output;
    }

    public String getTransformation()
    {
        return this.transformation;
    }

    /** Returns the classification rule being used to classify
     * pulse volumes.
     * @return Pulse Volume ClassificationRule
     * @see ClassificationRule
     * @see ThresholdRule
     * @see MajorityRule
     */
    public ClassificationRule getPulseClassificationRule()
    {
        return pulseClassificationRule;
    }

    /** Returns the classification rule being used to classify
     * sweeps.
     * @return Sweep ClassificationRule
     * @see ClassificationRule
     * @see ThresholdRule
     * @see MajorityRule
     */
    public ClassificationRule getSweepClassificationRule()
    {
        return sweepClassificationRule;
    }

    /**
     * Processes one instance using the pulse volume classification rule.
     * 
     * @param data instance to classify
     * @param distribution probability distribution for this instance
     */
    public void process(Instance data, double[] distribution)
    {
        freqs[pulseClassificationRule.getClassFromDistribution(distribution)]++;
        trueValue = (trueValue == -1) ? (int)data.classValue() : trueValue;
    }

    public void setExperimentDocument(Document experiment)
    {
        this.experiment = experiment;
    }

    /**Sets the pulse volume classification rule
     * @param rule Pulse Volume ClassificationRule
     * @see ClassificationRule
     * @see ThresholdRule
     * @see MajorityRule
     */
    public void setPulseClassificationRule(ClassificationRule rule)
    {
        pulseClassificationRule = rule;
    }

    /**Sets the sweep classification rule
     * @param rule Sweep ClassificationRule
     * @see ClassificationRule
     * @see ThresholdRule
     * @see MajorityRule
     */
    public void setSweepClassificationRule(ClassificationRule rule)
    {
        sweepClassificationRule = rule;
    }

    /** Sets the number of potential classes.
     * @param numClasses number of potential classes
     */
    public void setNumClasses(int numClasses)
    {
        this.numClasses = numClasses;
    }

    /** Set the output file location
     * @param output the output location
     */
    public void setOutput(String output)
    {
        this.output = output;
    }

    /** Set the transformation to use
     * @param transformation location of XSL transform file
     */
    public void setTransformation(String transformation)
    {
        this.transformation = transformation;
    }


    private void transformDocument(Document doc)
    {
        try
        {
            Transformer trans = null;

            TransformerFactory factory = TransformerFactory.newInstance();
            factory.setAttribute("indent-number", new Integer(3));
            if(!this.transformation.equals(""))
            {
                javax.xml.transform.Source xsltSource =
                    new javax.xml.transform.stream.StreamSource(
                            new File(this.transformation)); 
                trans = factory.newTransformer(xsltSource);
            }
            else
            {
                trans = factory.newTransformer();
            }
            trans.setOutputProperty(OutputKeys.INDENT, "yes");
            trans.transform(new DOMSource(doc), new StreamResult(new File(this.output)));
        }
        catch(Exception e)
        {
            System.out.println("XmlUtilityError: " + e.getMessage());
            System.out.println(e.getStackTrace());
        }
    }

}
